package com.hrsaas.ext.spider.core;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hrsaas.ext.spider.core.url.URL;
import com.hrsaas.ext.spider.err.Exceptions;
import com.hrsaas.ext.spider.util.Assert;
import com.hrsaas.ext.spider.util.TimeUtil;

import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;

/**
 * <p>
 * 功能：cookie要做到可以持久化,同一域名下传递
 * <p>
 * 
 * @author wangxiaoliang@kanzhun.com
 *
 *         2018年8月1日
 */
public final class CookieStore implements Iterable<Cookie> {

	final Set<DateCookie> cookies = new HashSet<>();
	private final ReadWriteLock lock = new ReentrantReadWriteLock();
	private final boolean checkDomain;
	private final boolean checkPath;
	private Receiver<Throwable> errorHandler;

	public CookieStore(boolean checkDomain, boolean checkPath) {
		this.checkDomain = checkDomain;
		this.checkPath = checkPath;
	}

	public CookieStore() {
		this(true, true);
	}

	CookieStore(Set<DateCookie> cookies, boolean checkDomain, boolean checkPath) {
		Assert.notNull("cookies", cookies);
		this.cookies.addAll(cookies);
		this.checkDomain = checkDomain;
		this.checkPath = checkPath;
	}

	public CookieStore checkDomain() {
		return new CookieStore(cookies, true, checkPath);
	}

	public CookieStore dontCheckDomain() {
		return new CookieStore(cookies, false, checkPath);
	}

	public int size() {
		Lock readLock = lock.readLock();
		try {
			readLock.lock();
			return cookies.size();
		} finally {
			readLock.unlock();
		}
	}

	public boolean isEmpty() {
		Lock readLock = lock.readLock();
		try {
			readLock.lock();
			return cookies.isEmpty();
		} finally {
			readLock.unlock();
		}
	}

	/**
	 * cookie path
	 * 
	 * @return
	 */
	public CookieStore checkPath() {
		return new CookieStore(cookies, checkDomain, true);
	}

	/**
	 * Returns a new CookieStore which does not check the path parameter of cookies
	 * when deciding to add them to a request.
	 *
	 * @return A new CookieStore
	 */
	public CookieStore dontCheckPath() {
		return new CookieStore(cookies, checkDomain, false);
	}

	/**
	 * Add a handler which will be called if an exception is thrown when parsing
	 * cookies - i.e. a server gave an invalid value for the cookie header. If not
	 * set, the exception will be thrown.
	 *
	 * @param errorHandler An error handler
	 * @return this
	 */
	public CookieStore errorHandler(Receiver<Throwable> errorHandler) {
		this.errorHandler = errorHandler;
		return this;
	}

	/**
	 * TODO
	 * @param req
	 */
	void decorate(HttpRequest req) {
		URL url = null;
		if (!req.uri().contains("://")) {
			String host = req.headers().get(HttpHeaderNames.HOST);
			url = URL.builder().setPath(req.uri()).setHost(host).create();
		} else {
			url = URL.parse(req.uri());
		}
		Lock readLock = lock.readLock();
		readLock.lock();
		try {
			List<Cookie> toSend = new ArrayList<>();
			for (Cookie cookie : cookies) {
				if (checkDomain) {
					if (cookie.domain() != null && !cookie.domain().equals(url.getHost())) {
						continue;
					}
				}
				if (checkPath) {
					String pth = cookie.path();
					if (pth != null) {
						String compare = url.getPath();
						if (!"/".equals(pth) && !"".equals(pth) && !compare.equals(pth)
								&& !compare.startsWith(pth)) {
							continue;
						}
					}
				}
				toSend.add(cookie);
			}
			if (!toSend.isEmpty()) {
				buildRequestCookie(req,toSend);
			}
		} finally {
			readLock.unlock();
		}
	}

	/**
	 * 无锁操作,针对响应的cookie再处理
	 * @param req
	 * @param toSend 
	 */
	void decorateAsync(HttpRequest req) {
		String host = req.headers().get(HttpHeaderNames.HOST);
		List<Cookie> toSend = new ArrayList<>();
		for (Cookie cookie : cookies) {
			if (host.contains(cookie.domain())) {
				toSend.add(cookie);
			}
		}
		buildRequestCookie(req,toSend);
	}
	
	/**
	 * 无锁操作,针对响应的cookie再处理，无域名判断
	 * @param req
	 */
	void buildRequestCookie(HttpRequest req,List<Cookie> cks) {
		if (!cks.isEmpty()) {
			StringBuilder cookievalues = new StringBuilder();
			for (Cookie ck : cks) {
				cookievalues.append(ck.name());
				cookievalues.append("=");
				cookievalues.append(ck.value());
				cookievalues.append(";");
			}
			req.headers().add(HttpHeaderNames.COOKIE, cookievalues.toString());
		}
	}
	
	public String get(String name) {
		Lock readLock = lock.readLock();
		readLock.lock();
		try {
			for (Cookie ck : cookies) {
				if (name.equals(ck.name())) {
					return ck.value();
				}
			}
		} finally {
			readLock.unlock();
		}
		return null;
	}

	public void add(Cookie cookie) {
		String name = cookie.name();
		Lock writeLock = lock.writeLock();
		try {
			writeLock.lock();
			for (Iterator<DateCookie> it = cookies.iterator(); it.hasNext();) {
				DateCookie ck = it.next();
				if (name.equals(ck.name()) 
						&& cookie.domain().equals(ck.domain())) {
					it.remove();
				} else if (ck.isExpired()) {
					it.remove();
				}
			}
			if (cookie.maxAge() > 0) {
				cookies.add(new DateCookie(cookie));
			}
		} finally {
			writeLock.unlock();
		}
	}

	public void remove(String name) {
		Lock writeLock = lock.writeLock();
		try {
			writeLock.lock();
			for (Iterator<DateCookie> it = cookies.iterator(); it.hasNext();) {
				DateCookie ck = it.next();
				if (name.equals(ck.name())) {
					it.remove();
				}
			}
		} finally {
			writeLock.unlock();
		}
	}

	/**
	 * 响应回来之后自动解析cookie，删除算定义cookie
	 * @param headers
	 */
	public void extract(HttpHeaders headers) {
		List<String> hdrs = headers.getAll(HttpHeaderNames.SET_COOKIE);
		if (!hdrs.isEmpty()) {
			Lock writeLock = lock.writeLock();
			try {
				writeLock.lock();
				for (String header : hdrs) {
					try {
						for (Cookie c : io.netty.handler.codec.http.CookieDecoder
								.decode(header)) {
							add(c);
						}
					} catch (Exception e) {
						if (errorHandler != null) {
							errorHandler.receive(e);
						} else {
							Exceptions.chuck(e);
						}
					}
				}
			} finally {
				writeLock.unlock();
			}
		}
	}

	/**
	 * 无锁操作处理cookie
	 * @param headers
	 */
	public void extractasync(HttpHeaders headers,String domain) {
		List<String> hdrs = headers.getAll(HttpHeaderNames.SET_COOKIE);
		if (!hdrs.isEmpty()) {
			for (String header : hdrs) {
				try {
					for (Cookie c : io.netty.handler.codec.http.CookieDecoder.decode(header)) {
						if (c.maxAge() < 0) {
							c.setMaxAge(Integer.MAX_VALUE);
						}
						if (c.domain() == null) {
							c.setDomain(domain);
						}
						cookies.add(new DateCookie(c));
					}
				} catch (Exception e) {
					if (errorHandler != null) {
						errorHandler.receive(e);
					} else {
						Exceptions.chuck(e);
					}
				}
			}
		}
	}
	
	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		for (DateCookie ck : cookies) {
			builder
				.append(ck.name())
				.append("=").append(ck.value()).append("; ")
				.append("Max-Age=").append(ck.maxAge()).append("; ")
				.append("Path=").append(ck.path()).append("; ")
				.append("Domain=").append(ck.domain()).append("; ");
		}
		return builder.toString();
	}

	@Override
	public Iterator<Cookie> iterator() {
		Lock readLock = lock.readLock();
		List<Cookie> cks = new ArrayList<>();
		readLock.lock();
		try {
			cks.addAll(cookies);
		} finally {
			readLock.unlock();
		}
		Collections.sort(cks);
		return cks.iterator();
	}

	public void store(OutputStream out) throws IOException {
		ObjectMapper om = new ObjectMapper();
		Lock readLock = lock.readLock();
		List<DateCookie> cks = new ArrayList<>();
		readLock.lock();
		try {
			cks.addAll(cookies);
		} finally {
			readLock.unlock();
		}
		List<Map<String, Object>> list = new LinkedList<>();
		for (DateCookie ck : cks) {
			Map<String, Object> m = new HashMap<>();
			m.put("domain", ck.domain());
			m.put("maxAge", ck.maxAge());
			m.put("timestamp", TimeUtil.toUnixTimestamp(ck.getTimestamp()));
			m.put("path", ck.path());
			m.put("name", ck.name());
			m.put("value", ck.value());
			m.put("httpOnly", ck.isHttpOnly());
			m.put("secure", ck.isSecure());
			list.add(m);
		}
		om.writeValue(out, list);
	}

	@SuppressWarnings("unchecked")
	public void read(InputStream in) throws IOException {
		ObjectMapper om = new ObjectMapper();
		List<Map<String, Object>> l = om.readValue(in, List.class);
		List<DateCookie> cks = new ArrayList<>();
		for (Map<String, Object> m : l) {
			String domain = (String) m.get("domain");
			Number maxAge = (Number) m.get("maxAge");
			Number timestamp = (Number) m.get("timestamp");
			String path = (String) m.get("path");
			String name = (String) m.get("name");
			String value = (String) m.get("value");
			Boolean httpOnly = (Boolean) m.get("httpOnly");
			Boolean secure = (Boolean) m.get("secure");
			String comment = (String) m.get("comment");
			String commentUrl = (String) m.get("commentUrl");
			List<Integer> ports = (List<Integer>) m.get("ports");
			ZonedDateTime ts = timestamp == null ? ZonedDateTime.now()
					: TimeUtil.fromUnixTimestamp(timestamp.longValue());
			DateCookie cookie = new DateCookie(new DefaultCookie(name, value), ts);
			if (cookie.isExpired()) {
				continue;
			}
			if (domain != null) {
				cookie.setDomain(domain);
			}
			if (maxAge != null) {
				cookie.setMaxAge(maxAge.longValue());
			}
			if (path != null) {
				cookie.setPath(path);
			}
			if (httpOnly != null) {
				cookie.setHttpOnly(httpOnly);
			}
			if (secure != null) {
				cookie.setSecure(secure);
			}
			cks.add(cookie);
		}
		if (!cks.isEmpty()) {
			Lock writeLock = lock.writeLock();
			try {
				writeLock.lock();
				cookies.addAll(cks);
			} finally {
				writeLock.unlock();
			}
		}
	}

	@Override
	public int hashCode() {
		return cookies.hashCode();
	}

	@Override
	public boolean equals(Object o) {
		if (o instanceof CookieStore) {
			return toString().equals(o.toString());
		}
		return false;
	}

	static final class DateCookie implements Cookie {

		private final Cookie delegate;
		private final ZonedDateTime timestamp;

		DateCookie(Cookie delegate) {
			this.delegate = delegate;
			timestamp = ZonedDateTime.now();
		}

		DateCookie(Cookie delegate, ZonedDateTime timestamp) {
			this.delegate = delegate;
			this.timestamp = timestamp;
		}

		public ZonedDateTime getTimestamp() {
			return timestamp;
		}

		public boolean isExpired() {
			if (maxAge() == Long.MAX_VALUE) {
				return false;
			}
			Duration dur;
			try {
				dur = Duration.ofSeconds(maxAge());
			} catch (ArithmeticException ex) {
				// A number high enough that * 1000 it is > Long.MAX_VALUE will overflow
				dur = Duration.ofDays(365 * 20);
			}
			try {
				return timestamp.plus(dur).isBefore(ZonedDateTime.now());
			} catch (DateTimeException e) {
				// This also can overflow
				return false;
			}
		}

		@Override
		public String name() {
			return delegate.name();
		}

		@Override
		public String value() {
			return delegate.value();
		}

		@Override
		public void setValue(String string) {
			delegate.setValue(string);
		}

		@Override
		public boolean wrap() {
			return delegate.wrap();
		}

		@Override
		public void setWrap(boolean bln) {
			delegate.setWrap(bln);
		}

		@Override
		public String domain() {
			return delegate.domain();
		}

		@Override
		public void setDomain(String string) {
			delegate.setDomain(string);
		}

		@Override
		public String path() {
			return delegate.path();
		}

		@Override
		public void setPath(String string) {
			delegate.setPath(string);
		}

		@Override
		public long maxAge() {
			return delegate.maxAge();
		}

		@Override
		public void setMaxAge(long l) {
			delegate.setMaxAge(l);
		}

		@Override
		public boolean isSecure() {
			return delegate.isSecure();
		}

		@Override
		public void setSecure(boolean bln) {
			delegate.setSecure(bln);
		}

		@Override
		public boolean isHttpOnly() {
			return delegate.isHttpOnly();
		}

		@Override
		public void setHttpOnly(boolean bln) {
			delegate.setHttpOnly(bln);
		}

		@Override
		public int compareTo(Cookie o) {
			return delegate.compareTo(o);
		}
	}

	public void addAll(CookieStore cks) {
		if (cks != null) {
			for (Cookie ck : cks) {
				this.add(ck);
			}
		}
	}

}
